package cn.cordys.crm.base;

import cn.cordys.common.constants.InternalUser;
import cn.cordys.common.exception.GenericException;
import cn.cordys.common.exception.IResultCode;
import cn.cordys.common.pager.Pager;
import cn.cordys.common.permission.PermissionCache;
import cn.cordys.common.uid.IDGenerator;
import cn.cordys.common.util.JSON;
import cn.cordys.common.util.LogUtils;
import cn.cordys.crm.system.domain.RolePermission;
import cn.cordys.crm.system.domain.User;
import cn.cordys.mybatis.BaseMapper;
import cn.cordys.security.SessionConstants;
import com.jayway.jsonpath.JsonPath;
import jakarta.annotation.Resource;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.TestMethodOrder;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.util.CollectionUtils;
import org.springframework.util.MultiValueMap;

import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;

import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public abstract class BaseTest {
    public static final String DEFAULT_ORGANIZATION_ID = "100001";
    protected static final String DEFAULT_USER_PASSWORD = "CordysCRM";
    protected static final String DEFAULT_PLATFORM = "mobile";
    protected static final String DEFAULT_PAGE = "page";
    protected static final String DEFAULT_LIST = "list";
    protected static final String DEFAULT_GET = "get/{0}";
    protected static final String DEFAULT_ADD = "add";
    protected static final String DEFAULT_UPDATE = "update";
    protected static final String DEFAULT_DELETE = "delete/{0}";
    protected static final String DEFAULT_BATCH_DELETE = "batch/delete";
    public static String PERMISSION_USER_NAME = "permission_test";
    protected static AuthInfo adminAuthInfo;
    protected static AuthInfo permissionAuthInfo;
    @Resource
    protected MockMvc mockMvc;
    @Resource
    protected PermissionCache permissionCache;
    @Resource
    private BaseMapper<RolePermission> rolePermissionMapper;
    @Resource
    private BaseMapper<User> userMapper;

    protected static Map<?, ?> parseResponse(MvcResult mvcResult) throws UnsupportedEncodingException {
        return JSON.parseMap(mvcResult.getResponse().getContentAsString(Charset.defaultCharset()));
    }

    private static MockMultipartFile getMockMultipartFile(String key, Object value) throws IOException {
        MockMultipartFile multipartFile;
        if (value instanceof File file) {
            multipartFile = new MockMultipartFile(key, file.getName(),
                    MediaType.APPLICATION_OCTET_STREAM_VALUE, Files.readAllBytes(file.toPath()));
        } else if (value instanceof MockMultipartFile) {
            multipartFile = (MockMultipartFile) value;
            // 有些地方的参数 name 写的是文件名，这里统一处理成参数名 key
            multipartFile = new MockMultipartFile(key, multipartFile.getOriginalFilename(),
                    MediaType.APPLICATION_OCTET_STREAM_VALUE, multipartFile.getBytes());
        } else {
            multipartFile = new MockMultipartFile(key, key,
                    MediaType.APPLICATION_JSON_VALUE, value.toString().getBytes());
        }
        return multipartFile;
    }

    protected String getBasePath() {
        return StringUtils.EMPTY;
    }

    @BeforeEach
    public void login() throws Exception {
        if (adminAuthInfo == null) {
            adminAuthInfo = initAuthInfo(InternalUser.ADMIN.getValue(), DEFAULT_USER_PASSWORD);
        }

        User permissionUser = userMapper.selectByPrimaryKey(PERMISSION_USER_NAME);
        // 有对应用户才初始化认证信息
        if (permissionUser != null && permissionAuthInfo == null) {
            permissionAuthInfo = initAuthInfo(PERMISSION_USER_NAME, DEFAULT_USER_PASSWORD);
        }
    }

    private AuthInfo initAuthInfo(String username, String password) throws Exception {
        MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.post("/login")
                        .content(String.format("{\"username\":\"%s\",\"password\":\"%s\",\"platform\":\"%s\"}", username, password, DEFAULT_PLATFORM))
                        .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andReturn();

        String sessionId = JsonPath.read(mvcResult.getResponse().getContentAsString(), "$.data.sessionId");
        String csrfToken = JsonPath.read(mvcResult.getResponse().getContentAsString(), "$.data.csrfToken");
        return new AuthInfo(sessionId, csrfToken);
    }

    protected MockHttpServletRequestBuilder getPostRequestBuilder(String url, Object param, Object... uriVariables) {
        return setRequestBuilderHeader(MockMvcRequestBuilders.post(getBasePath() + url, uriVariables), adminAuthInfo)
                .content(JSON.toJSONString(param))
                .contentType(MediaType.APPLICATION_JSON);
    }

    private MockHttpServletRequestBuilder setRequestBuilderHeader(MockHttpServletRequestBuilder requestBuilder, AuthInfo authInfo) {
        return requestBuilder
                .header(SessionConstants.HEADER_TOKEN, authInfo.getSessionId())
                .header(SessionConstants.CSRF_TOKEN, authInfo.getCsrfToken())
                .header("Organization-Id", DEFAULT_ORGANIZATION_ID) //TODO 暂时加上默认的组织ID
                .header(HttpHeaders.ACCEPT_LANGUAGE, "zh-CN");
    }

    protected ResultActions requestPost(String url, Object param, Object... uriVariables) throws Exception {
        return mockMvc.perform(getPostRequestBuilder(url, param, uriVariables))
                .andExpect(content().contentType(MediaType.APPLICATION_JSON));
    }

    protected MvcResult requestPostWithOkAndReturn(String url, Object param, Object... uriVariables) throws Exception {
        return this.requestPostWithOk(url, param, uriVariables).andReturn();
    }

    protected ResultActions requestPostWithOk(String url, Object param, Object... uriVariables) throws Exception {
        return mockMvc.perform(getPostRequestBuilder(url, param, uriVariables))
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk());
    }

    protected MockHttpServletRequestBuilder getRequestBuilder(String url, Object... uriVariables) {
        try {
            MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.get(getBasePath() + url, uriVariables);
            return setRequestBuilderHeader(requestBuilder, adminAuthInfo);
        } catch (Exception e) {
            LogUtils.error(e);
        }
        return null;
    }

    protected ResultActions requestGetWithOk(String url, Object... uriVariables) throws Exception {
        return mockMvc.perform(getRequestBuilder(url, uriVariables))
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk());
    }

    protected ResultActions requestGetStreamWithOk(String url, Object... uriVariables) throws Exception {
        return mockMvc.perform(getRequestBuilder(url, uriVariables))
                .andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM))
                .andExpect(status().isOk());
    }

    protected MvcResult requestGetWithOkAndReturn(String url, Object... uriVariables) throws Exception {
        return this.requestGetWithOk(url, uriVariables).andReturn();
    }

    protected <T> List<T> getResultDataArray(MvcResult mvcResult, Class<T> clazz) throws Exception {
        Object data = parseResponse(mvcResult).get("data");
        return JSON.parseArray(JSON.toJSONString(data), clazz);
    }

    protected <T> T getResultData(MvcResult mvcResult, Class<T> clazz) throws Exception {
        Object data = parseResponse(mvcResult).get("data");
        return JSON.parseObject(JSON.toJSONString(data), clazz);
    }

    protected <T> Pager<List<T>> getPageResult(MvcResult mvcResult, Class<T> clazz) throws Exception {
        Map<String, Object> pagerResult = (Map<String, Object>) parseResponse(mvcResult).get("data");
        List<T> list = JSON.parseArray(JSON.toJSONString(pagerResult.get("list")), clazz);
        Pager pager = new Pager();
        pager.setPageSize(Long.valueOf(pagerResult.get("pageSize").toString()));
        pager.setCurrent(Long.valueOf(pagerResult.get("current").toString()));
        pager.setTotal(Long.valueOf(pagerResult.get("total").toString()));
        pager.setList(list);
        return pager;
    }

    protected ResultActions requestGet(String url, Object... uriVariables) throws Exception {
        return mockMvc.perform(getRequestBuilder(url, uriVariables))
                .andExpect(content().contentType(MediaType.APPLICATION_JSON));
    }

    protected void requestPostPermissionTest(String permissionId, String url, Object param, Object... uriVariables) throws Exception {
        requestPermissionTest(permissionId, url, () -> getPermissionPostRequestBuilder(url, param, uriVariables));
    }

    protected void requestGetPermissionTest(String permissionId, String url, Object... uriVariables) throws Exception {
        requestPermissionTest(permissionId, url, () -> getPermissionRequestBuilder(url, uriVariables));
    }

    protected void requestPostPermissionsTest(List<String> permissionIds, String url, Object param, Object... uriVariables) throws Exception {
        requestPermissionsTest(permissionIds, url, () -> getPermissionPostRequestBuilder(url, param, uriVariables), mockMvc);
    }

    protected void requestGetPermissionsTest(List<String> permissionIds, String url, Object... uriVariables) throws Exception {
        requestPermissionsTest(permissionIds, url, () -> getPermissionRequestBuilder(url, uriVariables), mockMvc);
    }

    private MockHttpServletRequestBuilder getPermissionRequestBuilder(String url, Object... uriVariables) {
        MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.get(getBasePath() + url, uriVariables);
        return setRequestBuilderHeader(requestBuilder, permissionAuthInfo);
    }

    protected MockHttpServletRequestBuilder getPermissionPostRequestBuilder(String url, Object param, Object... uriVariables) {
        return setRequestBuilderHeader(MockMvcRequestBuilders.post(getBasePath() + url, uriVariables), permissionAuthInfo)
                .content(JSON.toJSONString(param))
                .contentType(MediaType.APPLICATION_JSON);
    }

    /**
     * 校验多个权限(同级别权限: 列如都是SYSTEM)
     *
     * @param permissionIds         多个权限
     * @param url                   请求url
     * @param requestBuilderGetFunc 请求构造器
     *
     * @throws Exception 请求抛出异常
     */
    public void requestPermissionsTest(List<String> permissionIds, String url, Supplier<MockHttpServletRequestBuilder> requestBuilderGetFunc, MockMvc mockMvc) throws Exception {
        for (String permissionId : permissionIds) {
            initUserRolePermission(permissionId);
        }

        // 刷新用户
        refreshUserPermission();

        int status = mockMvc.perform(requestBuilderGetFunc.get())
                .andReturn()
                .getResponse()
                .getStatus();

        // 校验是否有权限
        if (status == HttpStatus.FORBIDDEN.value()) {
            throw new GenericException(String.format("接口 %s 权限校验失败 %s", url, permissionIds));
        }

        // 删除权限
        RolePermission example = new RolePermission();
        example.setRoleId(PERMISSION_USER_NAME);
        rolePermissionMapper.delete(example);

        // 删除后刷新下权限
        refreshUserPermission();

        // 删除权限后，调用接口，校验是否没有权限
        status = mockMvc.perform(requestBuilderGetFunc.get())
                .andReturn()
                .getResponse()
                .getStatus();

        if (status != HttpStatus.FORBIDDEN.value()) {
            throw new GenericException(String.format("接口 %s 没有设置权限 %s", url, permissionIds));
        }
    }

    /**
     * 校验权限
     * 实现步骤
     * 1. 在 application.properties 配置权限的初始化 sql
     * spring.sql.init.mode=always
     * spring.sql.init.schema-locations=classpath*:dml/init_permission_test.sql
     * 2. 在 init_permission_test.sql 中配置权限，
     * 3. 向该用户组中添加权限测试是否生效，删除权限测试是否可以访问
     *
     * @param permissionId
     * @param url
     * @param requestBuilderGetFunc 请求构造器，一个 builder 只能使用一次，需要重新生成
     *
     * @throws Exception
     */
    public void requestPermissionTest(String permissionId, String url, Supplier<MockHttpServletRequestBuilder> requestBuilderGetFunc) throws Exception {
        // 先给初始化的用户组添加权限
        RolePermission userRolePermission = initUserRolePermission(permissionId);

        // 添加后刷新下权限
        refreshUserPermission();

        int status = mockMvc.perform(requestBuilderGetFunc.get())
                .andReturn()
                .getResponse()
                .getStatus();

        // 校验是否有权限
        if (status == HttpStatus.FORBIDDEN.value()) {
            throw new GenericException(String.format("接口 %s 权限校验失败 %s", url, permissionId));
        }

        // 删除权限
        rolePermissionMapper.deleteByPrimaryKey(userRolePermission.getId());

        // 删除后刷新下权限
        refreshUserPermission();

        // 删除权限后，调用接口，校验是否没有权限
        status = mockMvc.perform(requestBuilderGetFunc.get())
                .andReturn()
                .getResponse()
                .getStatus();
        if (status != HttpStatus.FORBIDDEN.value()) {
            throw new GenericException(String.format("接口 %s 没有设置权限 %s", url, permissionId));
        }
    }

    /**
     * 校验错误响应码
     */
    protected void assertErrorCode(ResultActions resultActions, IResultCode resultCode) throws Exception {
        resultActions
                .andExpect(
                        jsonPath("$.code")
                                .value(resultCode.getCode())
                );
    }

    /**
     * 给用户组绑定对应权限
     *
     * @param permissionId
     *
     * @return
     */
    private RolePermission initUserRolePermission(String permissionId) {
        RolePermission rolePermission = new RolePermission();
        rolePermission.setRoleId(PERMISSION_USER_NAME);
        rolePermission.setId(IDGenerator.nextStr());
        rolePermission.setPermissionId(permissionId);
        rolePermissionMapper.insert(rolePermission);
        return rolePermission;
    }

    /**
     * 调用 is-login 接口刷新权限
     *
     * @throws Exception
     */
    private void refreshUserPermission() {
        permissionCache.clearCache(PERMISSION_USER_NAME, DEFAULT_ORGANIZATION_ID);
    }

    protected ResultActions requestMultipart(String url, MultiValueMap<String, Object> paramMap, Object... uriVariables) throws Exception {
        MockHttpServletRequestBuilder requestBuilder = getMultipartRequestBuilder(url, paramMap, uriVariables);
        return mockMvc.perform(requestBuilder)
                .andExpect(content().contentType(MediaType.APPLICATION_JSON));
    }


    private MockHttpServletRequestBuilder getMultipartRequestBuilder(String url,
                                                                     MultiValueMap<String, Object> paramMap,
                                                                     Object[] uriVariables) {
        MockMultipartHttpServletRequestBuilder requestBuilder = getMultipartRequestBuilderWithParam(url, paramMap, uriVariables);
        return setRequestBuilderHeader(requestBuilder, adminAuthInfo)
                .header(SessionConstants.HEADER_TOKEN, adminAuthInfo.getSessionId())
                .header(SessionConstants.CSRF_TOKEN, adminAuthInfo.getCsrfToken());
    }

    /**
     * 构建 multipart 带参数的请求
     *
     * @param url
     * @param paramMap
     * @param uriVariables
     *
     * @return
     */
    private MockMultipartHttpServletRequestBuilder getMultipartRequestBuilderWithParam(String url, MultiValueMap<String, Object> paramMap, Object[] uriVariables) {
        MockMultipartHttpServletRequestBuilder requestBuilder =
                MockMvcRequestBuilders.multipart(getBasePath() + url, uriVariables);
        paramMap.forEach((key, value) -> {
            List list = value;
            for (Object o : list) {
                try {
                    if (o == null) {
                        continue;
                    }
                    MockMultipartFile multipartFile;
                    if (o instanceof List listObject) {
                        if (CollectionUtils.isEmpty(listObject)) {
                            continue;
                        }
                        if (listObject.getFirst() instanceof File || listObject.getFirst() instanceof MockMultipartFile) {
                            // 参数是多个文件时,设置多个文件
                            for (Object subObject : listObject) {
                                multipartFile = getMockMultipartFile(key, subObject);
                                requestBuilder.file(multipartFile);
                            }
                        } else {
                            multipartFile = getMockMultipartFile(key, o);
                            requestBuilder.file(multipartFile);
                        }
                    } else {
                        multipartFile = getMockMultipartFile(key, o);
                        requestBuilder.file(multipartFile);
                    }
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }

            }
        });
        return requestBuilder;
    }

    @Data
    public static class AuthInfo {
        private String sessionId;
        private String csrfToken;

        public AuthInfo(String sessionId, String csrfToken) {
            this.sessionId = sessionId;
            this.csrfToken = csrfToken;
        }
    }
}
